package fr.vahren.sriksal.generation;

import java.awt.image.BufferedImage;
import java.util.ArrayList;

import fr.vahren.sriksal.generation.DropStrategy.Direction;

public class RiverSpawnerStrategy implements RiverGenerationStrategy {

	private static final int NB_SOURCES = 30;
	private static final int INERTIA = 1;
	private static final int MINIMUM_RIVER_LENGTH = 80;
	private static final int MIN_ALT_FOR_SOURCE = 60;
	private static final int RIVER_DEPTH = 2;
	private static final int SEA_LEVEL = 90;
	private static final double CHANGE_DIR_CHANCE = 0.3;
	private ArrayList<Position> sources;
	private ArrayList<ArrayList<Position>> paths;
	private int maxAlt = 255;
	private int max;

	private int[][] waterMap;

	public RiverSpawnerStrategy() {
		sources = new ArrayList<Position>();
		paths = new ArrayList<ArrayList<Position>>();
	}

	@Override
	public void init(int[][] map) {
		// Init river start points
		// Get max elevation
		max = 0;
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[i].length; j++) {
				if (map[i][j] > max) {
					max = map[i][j];
				}
			}
		}
		waterMap = new int[map.length][map[0].length];
	}

	@Override
	public void generateRivers(int[][] map, int[][] terrainMap) {
		// spawn NB_SOURCES sources
		int alt = 0, x = -1, y = -1;
		while (sources.size() < NB_SOURCES) {
			x = (int) (Math.random() * map.length);
			y = (int) (Math.random() * map[0].length);
			alt = map[x][y];
			if (isSourceValid(x, y, alt)) {
				Position source = new Position(x, y, map[x][y]);

				// for each source
				RiverDrop d;
				// create a drop, and store its path until it evaporates
				ArrayList<Position> path = new ArrayList<Position>();
				d = new RiverDrop(source, 20, 2, 1);

				Direction nextDir = getDirection(map, d.getPosition().getX(), d
						.getPosition().getY(), d.getPosition().getH(),
						d.getDirection(), d);

				Position next = getNextPosition(d, map, nextDir);
				while (next != null && !riverHasAlreadyPassedHere(path, next)) {
					// MOVE
					path.add(next);
					move(d, next, nextDir);

					// GET NEXT
					nextDir = getDirection(map, d.getPosition().getX(), d
							.getPosition().getY(), d.getPosition().getH(),
							d.getDirection(), d);
					next = getNextPosition(d, map, nextDir);
				}
				// path must be long enough, and under sea level at the end.
				if (path.size() > MINIMUM_RIVER_LENGTH
						&& path.get(path.size() - 1).getH() <= SEA_LEVEL) {
					// if it is ok erode the path.
					// ERODE height and terrain!
					erode(path, map, terrainMap);
					paths.add(path);
					sources.add(source);
				}
			}
		}

	}

	/**
	 * Erode the point the river is at. Erode all around in 4-connexity.
	 * 
	 * @param path
	 * @param map
	 * @param terrainMap
	 */
	private void erode(ArrayList<Position> path, int[][] map, int[][] terrainMap) {
		for (Position step : path) {
			erodeUp(step, map, terrainMap);
			erodeLeft(step, map, terrainMap);
			erodeRight(step, map, terrainMap);
			erodeDown(step, map, terrainMap);
		}
	}

	private void erodeDown(Position step, int[][] map, int[][] terrainMap) {
		int x = step.getX();
		int y = step.getY();
		if (x < map.length - 1) {
			waterMap[x + 1][y] = map[x + 1][y];
			remove(map, x + 1, y, RIVER_DEPTH);
			remove(terrainMap, x + 1, y, RIVER_DEPTH);
		}
	}

	private void remove(int[][] map, int i, int j, int riverDepth) {
		map[i][j] = Math.max(0, map[i][j] - riverDepth);
	}

	private void erodeRight(Position step, int[][] map, int[][] terrainMap) {
		int x = step.getX();
		int y = step.getY();
		if (y < map[0].length - 1) {
			waterMap[x][y + 1] = map[x][y + 1];
			remove(map, x, y + 1, RIVER_DEPTH);
			remove(terrainMap, x, y + 1, RIVER_DEPTH);
		}
	}

	private void erodeLeft(Position step, int[][] map, int[][] terrainMap) {
		int x = step.getX();
		int y = step.getY();
		if (y > 0) {
			waterMap[x][y - 1] = map[x][y - 1];
			remove(map, x, y - 1, RIVER_DEPTH);
			remove(terrainMap, x, y - 1, RIVER_DEPTH);
		}
	}

	private void erodeUp(Position step, int[][] map, int[][] terrainMap) {
		int x = step.getX();
		int y = step.getY();
		if (x > 0) {
			waterMap[x - 1][y] = map[x - 1][y];
			remove(map, x - 1, y, RIVER_DEPTH);
			remove(terrainMap, x - 1, y, RIVER_DEPTH);
		}
	}

	private boolean riverHasAlreadyPassedHere(ArrayList<Position> path,
			Position next) {
		for (Position p : path) {
			if (p.getX() == next.getX() && p.getY() == next.getY()) {
				return true;
			}
		}
		return false;
	}

	private void move(RiverDrop d, Position next, Direction dir) {
		int delta = next.getH() - d.getPosition().getH();
		d.modifyInertia(delta);
		d.setPosition(next);
		d.setDirection(dir);
	}

	private boolean isSourceValid(int x, int y, int alt) {
		return alt > (max - MIN_ALT_FOR_SOURCE);
	}

	/**
	 * 
	 * @param drop
	 * @param map
	 * @param nextDir
	 * @return null if there is nowhere to go, or the next position of the drop.
	 */
	public Position getNextPosition(RiverDrop drop, int[][] map,
			Direction nextDir) {
		int x = drop.getPosition().getX();
		int y = drop.getPosition().getY();
		switch (nextDir) {
		case NOWHERE:
			// Nowhere to go upward!
			return null;
		case UP:
			return new Position(x - INERTIA, y, map[x - INERTIA][y]);
		case LEFT:
			return new Position(x, y - INERTIA, map[x][y - INERTIA]);
		case RIGHT:
			return new Position(x, y + INERTIA, map[x][y + INERTIA]);
		case DOWN:
			return new Position(x + INERTIA, y, map[x + INERTIA][y]);
		default:
			break;
		}
		return null;
	}

	/**
	 * Get next direction. If there is nowhere to go but up, or if it touch the
	 * border, returns {@link Direction#NOWHERE}.
	 * 
	 * @param map
	 * @param x
	 * @param y
	 * @param h
	 * @return
	 */
	private Direction getDirection(int[][] map, int x, int y, int h,
			Direction d, RiverDrop drop) {
		int altUp = maxAlt, altLeft = maxAlt, altRight = maxAlt, altDown = maxAlt;
		// Up x-1
		if (x >= INERTIA) {
			altUp = map[x - INERTIA][y];
		} else {
			return Direction.NOWHERE;
		}
		// Left y-1
		if (y >= INERTIA) {
			altLeft = map[x][y - INERTIA];
		} else {
			return Direction.NOWHERE;
		}
		// Right y+1
		if (y < Generator.WIDTH - INERTIA) {
			altRight = map[x][y + INERTIA];
		} else {
			return Direction.NOWHERE;
		}
		// Down x+1
		if (x < Generator.HEIGHT - INERTIA) {
			altDown = map[x + INERTIA][y];
		} else {
			return Direction.NOWHERE;
		}

		int[] alts = { altUp, altLeft, altRight, altDown };
		// Check if the drop can continue in its direction but only in descent
		if (d != Direction.NOWHERE
				&& drop.getPosition().getH() >= alts[d.ordinal()]
				&& drop.canGo(alts[d.ordinal()])
				&& Math.random() > CHANGE_DIR_CHANCE) {
			return d;
		} else {
			// else Find the minimum alts
			int minAlt = maxAlt;
			ArrayList<Direction> direction = new ArrayList<Direction>();
			for (int i = 0; i < alts.length; i++) {
				if (alts[i] < minAlt - 5) {
					minAlt = alts[i]+5;
					direction.clear();
					direction.add(Direction.values()[i]);
				} else if (alts[i] > minAlt) {
					// nothing
				} else {
					direction.add(Direction.values()[i]);
				}
			}
			// Check if the drop can go there
			if (drop.canGo(minAlt)) {
				// Get randomly one of the lowest directions
				return direction.get((int) Math.random() * direction.size());
			} else {
				return Direction.NOWHERE;
			}
		}
	}


	@Override
	public void fillSeas(int[][] map) {
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[i].length; j++) {
				if (map[i][j] <= SEA_LEVEL) {
					waterMap[i][j] = SEA_LEVEL;
				}
			}
		}
	}

	@Override
	public int[][] getWaterMap() {
		return waterMap;
	}

	@Override
	public void updateAllMap(int[][] waterMap, int[][] map, int[][] terrainMap,
			BufferedImage image) {
		int color, red, green, blue;
		// grey scale altitude
		for (int i = 0; i < waterMap.length; i++) {
			for (int j = 0; j < waterMap[i].length; j++) {
				if (waterMap[i][j] == 0) {
					// draw terrain (earth/stone)
					red = terrainMap[i][j] > 0 ? 0 : map[i][j];
					green = terrainMap[i][j] > 0 ? map[i][j] : 0;
					blue = 0;
				} else {
					red = 0;
					green = 0;
					blue = waterMap[i][j];
				}
				color = ((red << 8) + green << 8) + blue;
				image.setRGB(i, j, color);
			}
		}
	}

}
